/*                                    _
 *   ___ _ __ ___ _ __ ___   ___   __| |
 *  / __| '__/ __| '_ ` _ \ / _ \ / _` |
 *  \__ \ |  \__ \ | | | | | (_) | (_| |
 *  |___/_|  |___/_| |_| |_|\___/ \__,_|                                  
 *
 *      _  _ _    _     _ _|_ _  _|_  _ _ |  _ 
 *   \/(/_| _\|_|_\  VV(_| | (/_| |_)| (_||<(/_          
 * 
 *              _ _  _  _|   | _ 
 *             | | |(_)(_||_||(/_
 *                     
 ******************************************
 ******************************************
 ******************************************
 * . _ _  _ | _  _ _  _  _ _|_ _ _|_. _  _ 
 * || | ||_)|(/_| | |(/_| | | (_| | |(_)| |
 *       |                                 
 *		
 *		Utilizes an OnGameFrame Hook (skipping some to save resources) 
 *		To check and Track Survivors "waterlevel" Entity Property.
 *		If a Survivor is found in Water he is slowed down.
 *		
 *		The Survivor Index was created in order to not iterate all Survivors
 *		again and again every Call, but only when needed.
 *		
 *		Also, we have some Eventhooks to optionally disable the slowdown
 *		on Survivors with a Jockey on them.
 */

#define MODULE_NAME "WaterBrake"

static const 		FRAMESKIP 				= 3;	// Our OnGameFrame function will only trigger every third Frame
static const		JUMPFLAG				= IN_JUMP;
static const		WATERFLAG				= FL_INWATER;
static const 		Float:FULL_SPEED		= 1.0;
static const 		Float:JUMP_PENALTY		= 0.25;	// gets added to the velocitymodifier on jumping

static const String:LIMP_HEALTH_CONVAR[]	= "survivor_limp_health";
static const String:PLAYER_CLASSNAME[]		= "CTerrorPlayer";
static const String:MOVE_SPEED_ENTPROP[]	= "m_flLaggedMovementValue";
static const String:SPEED_MODIFY_ENTPROP[]	= "m_flVelocityModifier";
 
static bool:isSlowedDown[MAXPLAYERS+1]		= false;
static bool:isJockeyed[MAXPLAYERS+1]		= false;
static bool:isAllowedMode					= false;
static bool:enableBrake 					= false;
static bool:eventsHooked					= false;

static enableLowHealthBrake					= 0;
static lowHealthLimitLimp					= 40;
static Float:slowSetting 					= 0.75;
static everyXFrame							= 0;
static laggedMovementOffset					= 0;
static velocityModifierOffset				= 0;

static Handle:cvarEnabled 					= INVALID_HANDLE;
static Handle:cvarWaterSlow 				= INVALID_HANDLE;
static Handle:cvarJockeySlow 				= INVALID_HANDLE;
static Handle:cvarLowHealthSlow				= INVALID_HANDLE;
static Handle:cvarLimpHealth				= INVALID_HANDLE;

WaterBrake_OnModuleLoaded()
{
	cvarEnabled = 		CreateConVar("srs_water_brake", 				"1", 	" Enable or Disable the Water Brake functionality ", 						SRS_CVAR_DEFAULT_FLAGS);
	cvarWaterSlow = 	CreateConVar("srs_water_brake_speed", 			"0.75", " How much Survivors will be slowed down in Water, 0.75 means 75% speed ", 	SRS_CVAR_DEFAULT_FLAGS);
	cvarJockeySlow = 	CreateConVar("srs_water_slow_jockeyed_ones", 	"0", 	" Will Water Brake be enabled for Jockeyed Survivors ", 					SRS_CVAR_DEFAULT_FLAGS);
	cvarLowHealthSlow = CreateConVar("srs_water_slow_at_low_health", 	"0", 	" Enable or Disable Water Brake for low Health Survivors ", 				SRS_CVAR_DEFAULT_FLAGS);
	
	cvarLimpHealth = FindConVar(LIMP_HEALTH_CONVAR);
	
	HookConVarChange(cvarEnabled, 			_WB_ConvarsChanged);
	HookConVarChange(cvarEnabled, 			_WB_ActivityChanged);
	HookConVarChange(cvarWaterSlow, 		_WB_ConvarsChanged);
	HookConVarChange(cvarLimpHealth, 		_WB_ConvarsChanged);
	HookConVarChange(cvarLowHealthSlow, 	_WB_ConvarsChanged);
	HookConVarChange(GetGameModeHandle(), 	_WB_ConvarsChanged);
	
	laggedMovementOffset = 		FindSendPropInfo(PLAYER_CLASSNAME, MOVE_SPEED_ENTPROP);
	velocityModifierOffset = 	FindSendPropInfo(PLAYER_CLASSNAME, SPEED_MODIFY_ENTPROP);
	
	_WB_OnModuleEnabled();
}

public _WB_ConvarsChanged(Handle:convar, const String:oldValue[], const String:newValue[])
{
	enableBrake = 			GetConVarBool(cvarEnabled);
	isAllowedMode = 		IsAllowedGameMode();
	slowSetting = 			GetConVarFloat(cvarWaterSlow);
	lowHealthLimitLimp = 	GetConVarInt(cvarLimpHealth);
	enableLowHealthBrake = 	GetConVarBool(cvarLowHealthSlow);
}
	
public _WB_ActivityChanged(Handle:convar, const String:oldValue[], const String:newValue[])
{
	if (ACTIVATION_FLIP_OFF_TO_ON)
	{
		_WB_OnModuleEnabled();
	}
	else if (ACTIVATION_FLIP_ON_TO_OFF)
	{
		_WB_OnModuleDisabled();
	}
	
	enableBrake = GetConVarBool(cvarEnabled);
}

static _WB_OnModuleEnabled()
{
	HookEvent("round_start", 		_WB_RoundStart_Event);
	HookEvent("round_end", 			_WB_RoundEnd_Event);
	HookEvent("jockey_ride", 		_WB_PlayerJockeyed_Event);
	HookEvent("jockey_ride_end", 	_WB_JockeyRideEnd_Event);
	eventsHooked = true;
}

static _WB_OnModuleDisabled()
{
	if (!IsPluginEnding() && eventsHooked)
	{
		UnhookEvent("round_start", 		_WB_RoundStart_Event);
		UnhookEvent("round_end", 		_WB_RoundEnd_Event);
		UnhookEvent("jockey_ride", 		_WB_PlayerJockeyed_Event);
		UnhookEvent("jockey_ride_end", 	_WB_JockeyRideEnd_Event);
		eventsHooked = false;
	}
	
	FOR_EACH_ALIVE_SURVIVOR_INDEXED(i)
	{
		if (isSlowedDown[i])
		{
			isSlowedDown[i] = false;
			SetEntDataFloat(i, laggedMovementOffset, FULL_SPEED, true);
		}
	}
}

public _WB_RoundStart_Event(Handle:event, const String:name[], bool:dontBroadcast)
{
	isAllowedMode = IsAllowedGameMode();

	FOR_EACH_CLIENT_COND(i, isJockeyed[i])
	{
		isJockeyed[i] = false;
	}
}

public _WB_RoundEnd_Event(Handle:event, const String:name[], bool:dontBroadcast)
{
	isAllowedMode = false;
}

public _WB_PlayerJockeyed_Event(Handle:event, const String:name[], bool:dontBroadcast)
{
	if (GetConVarBool(cvarJockeySlow)) return;

	new victim = GetClientOfUserId(GetEventInt(event, "victim"));
	if (!victim || !IsClientInGame(victim)) return;
	
	isJockeyed[victim] = true;
	
	if (isSlowedDown[victim]) // if a slow was in effect already
	{
		isSlowedDown[victim] = false;
		SetEntDataFloat(victim, laggedMovementOffset, FULL_SPEED, true); // reset plugin saved status and ingame speed
	}
}

public _WB_JockeyRideEnd_Event(Handle:event, const String:name[], bool:dontBroadcast)
{
	new victim = GetClientOfUserId(GetEventInt(event, "victim"));
	if (!victim || !IsClientInGame(victim)) return;
	
	isJockeyed[victim] = false;
}

WaterBrake_OnGameFrame()
{
	if (!IsServerProcessing()) return;

	if (!isAllowedMode || !enableBrake) return;
	
	if (!survivorCount) return;
	
	everyXFrame++;
	if (everyXFrame >= FRAMESKIP)
	{
		everyXFrame = 1;
		
		FOR_EACH_ALIVE_SURVIVOR_INDEXED(i)
		{
			new flags = GetEntityFlags(i);
			
			if (flags & WATERFLAG) // case Survivor in water
			{
				if (!isSlowedDown[i] && !isJockeyed[i]) // and not jockeyed and not yet slowed
				{
					if (!enableLowHealthBrake && L4D2_GetSurvivorAbsHealth(i) < lowHealthLimitLimp) return; // if Survivor Health is low and Low Health Brake not enabled return
					
					isSlowedDown[i] = true;
					SetEntDataFloat(i, laggedMovementOffset, slowSetting, true); // tell the plugin the Survivor is now slow and apply it
				}
				
				else if (isSlowedDown[i] && isJockeyed[i]) // is slowed but got jockeyed (now)
				{
					isSlowedDown[i] = false;
					SetEntDataFloat(i, laggedMovementOffset, FULL_SPEED, true); // remove slowdown
				}
			}
			
			else if (isSlowedDown[i]) // case Survivor not in water but slowed
			{
				isSlowedDown[i] = false;
				SetEntDataFloat(i, laggedMovementOffset, FULL_SPEED, true); // remove slowdown
				
				if (flags & JUMPFLAG) // if the survivor is jumping aswell
				{
					SetEntDataFloat(i, velocityModifierOffset, slowSetting - JUMP_PENALTY, true);	// apply velocity modifier with penalty so bunny hopping wont make you faster
																									// note: does not take effect until you touch ground again. pretty realistic actually.
					
					// also note: i could make Water Brake work with the velocity modifier entirely, but it causes microstuttering. laggedMovement is smooth
					// fun fact: whenever youre hit by a zombie you actually get microstuttering, valve covers it up by shaking your view
				}
			}
		}
	}
}

#undef MODULE_NAME